Defering jobs enqueuening to after the transaction commit, queries count in rendering logs and more | This Week in Rails
ActiveRecordに関する変更です
ActiveRecordのトランザクションにはいつもお世話になっています
今回のプルリクエストでは ActiveRecord::Base.transaction に渡すブロックの引数に ActiveRecord::Transaction のオブジェクトを受け取れるようになりました
そしてそのTransactionオブジェクトに対してコールバックを登録できるようになっています
具体例をみてみましょう
code:rb
Article.transaction do |transaction|
article.update(published: true)
transaction.after_commit do
PublishNotificationMailer.with(article: article).deliver_later
end
end
Article.transaction に渡すブロックについて引数 transaction が受け取れる
transaction に対してafter_commit メソッドで、トランザクションが commit されたあとに実行する処理を登録してる
「articleの更新が完了したあとにメールを送る」という処理
また、ActiveRecord::Base.current_transaction も追加されています
これにより、このメソッドの呼び出し時点におけるTransactionオブジェクトを扱える様になっています
先程の例を書き換えるならばこうなりますね
code:rb
Article.transaction do
article.update(published: true)
Article.current_transaction.after_commit do
PublishNotificationMailer.with(article: article).deliver_later
end
end
先ほどの例における、ブロックの引数 transaction を使わなくても現在のトランザクションが分かるので、例えば処理を別メソッドに切り出すときなどに便利ですね
code:rb
def foo
Article.transaction do
article.update(published: true)
send_mail
end
end
private
def send_mail
Article.current_transaction.after_commit do
PublishNotificationMailer.with(article: article).deliver_later
end
end
さらに ActiveRecord.after_all_transactions_commit も追加されています
このメソッドはブロックを受け取り、そのブロックの処理はすべてのトランザクションがコミットされたあとに実行されます
例えば先ほどの例で、記事の公開とお知らせ通知メールを1つのメソッド(publish_article メソッド)にまとめた場合、このメソッドは次のような状況に置かれます
トランザクションの中から呼び出されるのか外から呼び出されるのか分からない
けれど、メール送信処理は記事の更新が完了した(DBに永続化された)あとに実行したい
そんなシチュエーションにおいて after_all_transactions_commit を使うと、「とにかくこのブロックの中はデータが永続化されたあとに実行してほしい」という処理を登録できるので便利ですね
code:rb
def publish_article(article)
article.update(published: true)
ActiveRecord.after_all_transactions_commit do
PublishNotificationMailer.with(article: article).deliver_later
end
end
ActiveJobに関する変更です
このプルリクエストは先ほど紹介したActiveRecord::Transactionに関する実装と関わりのあるプルリクエストです
ActiveJobに関して陥りやすいミスとして、DBトランザクションのなかからJobのキューを積んでしまうことが挙げられます
トランザクションがコミットされる前に別のプロセスがジョブを実行してしまう可能性があり、結果としてさまざまなエラーにつながります
先ほどの「記事の公開とお知らせ通知メール」の例で解説します
code:rb
Article.transaction do
article.update(published: true)
PublishNotificationMailer.with(article: article).deliver_later
end
トランザクションの中でarticleを更新し、公開をお知らせするメールジョブを積んでいます
トランザクション内の後続の処理が終わる前に(つまり、aritcleが公開される前に)、お知らせジョブが別のプロセスで実行されてしまう可能性があります
また、仮にトランザクション内の後続の処理でエラーが発生した場合、articleの公開もロールバックされて非公開に戻るため、「記事は非公開なのに公開お知らせメールをユーザに送ってしまった状態」になり得ます
このような状況を回避するためには、ジョブを積む処理をトランザクションの外に出す、あるいは、先ほど紹介した ActiveRecord.after_all_transactions_commit をつかってトランザクションのコミットが完了したあとにジョブが積まれるようにする、という対応が必要です
そこで、今回のプルリクエストでは、自動的にトランザクションがコミットされるまでジョブを積むのを遅延するようになりました
つまり、意識して上記のような対応をする必要がなくなった、ということです
一方で、トランザクションのコミットを待たずにジョブをエンキューしたい場合を考慮する必要が出てきました
そのケースに対応するために、各ジョブのクラスに enqueue_after_transaction_commit というclass_attributeを設定できるようになっています
このattributeに false を指定することでトランザクションのコミットを待たずにジョブをエンキューできます
code:rb
class FooBarJob < ApplicationJob
self.enqueue_after_transaction_commit = false
end
ActionViewに関する変更です
CHENGELOGとしてはActionView,実装内容としてはActiveRecordに関する変更です
アクション単位で出力されるログについて、時間だけでなくActiveRecordのクエリの件数が表示されるようになりました
code:log
# Before
Completed 200 OK in 3804ms (Views: 41.0ms | ActiveRecord: 33.5ms | Allocations: 112788)
# After
Completed 200 OK in 3804ms (Views: 41.0ms | ActiveRecord: 33.5ms (2 queries, 1 cached) | Allocations: 112788)